/* $Id: RPServerManager.java,v 1.74 2010/12/13 20:30:11 kymara Exp $ */
/***************************************************************************
 *                      (C) Copyright 2003 - Marauroa                      *
 ***************************************************************************
 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
package marauroa.server.game.rp;

import games.mapacman.server.MaPacmanRPWorld;
import games.mapacman.server.ZoneDivider;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import marauroa.common.Configuration;
import marauroa.common.Log4J;
import marauroa.common.game.AccountResult;
import marauroa.common.game.CharacterResult;
import marauroa.common.game.IRPZone;
import marauroa.common.game.Perception;
import marauroa.common.game.RPAction;
import marauroa.common.game.RPObject;
import marauroa.common.game.RPObjectInvalidException;
import marauroa.common.game.RPObjectNotFoundException;
import marauroa.common.game.Result;
import marauroa.common.net.message.MessageS2CMigrate;
import marauroa.common.net.message.MessageS2CPerception;
import marauroa.common.net.message.MessageS2CTransferREQ;
import marauroa.common.net.message.TransferContent;
import marauroa.server.db.TransactionPool;
import marauroa.server.game.ActionInvalidException;
import marauroa.server.game.SimulationStats;
import marauroa.server.game.Statistics;
import marauroa.server.game.container.ClientState;
import marauroa.server.game.container.PlayerEntry;
import marauroa.server.game.container.PlayerEntryContainer;
import marauroa.server.game.db.AccountDAO;
import marauroa.server.game.db.CharacterDAO;
import marauroa.server.game.db.DAORegister;
import marauroa.server.net.INetworkServerManager;
import marauroa.server.net.validator.ConnectionValidator;

/**
 * This class is responsible for adding actions to scheduler, and to build and
 * sent perceptions.
 * <p>
 * The goal of the RP Manager is to handle the RP of the game. This means:
 * <ul>
 * <li>run RPActions from clients
 * <li>manage RPWorld
 * <li>control triggers for events
 * <li>control AI
 * </ul>
 * <p>
 * This is a HUGE task that is very complex.<br>
 * Hence we split that behaviour in different class:
 * <ul>
 * <li>IRuleProcessor will handle al the RP logic and run actions from client.
 * This class will also implement AI, triggers, events, rules, etc...
 * <li>RPWorld will handle all the world storage and management logic.
 * </ul>
 *
 * @author miguel
 */
public class RPServerManager extends Thread {

    /** the logger instance. */
    private static final marauroa.common.Logger logger = Log4J.getLogger(RPServerManager.class);
    /** The thread will be running while keepRunning is true */
    private volatile boolean keepRunning;
    /** isFinished is true when the thread has really exited. */
    private volatile boolean isfinished;
    /** The time elapsed between 2 turns. */
    private long turnDuration;
    /** The number of the turn that we are executing now */
    private int turn;
    /** The scheduler needed to organize actions */
    private RPScheduler scheduler;
    /** The ruleProcessor that the scheduler will use to execute the actions */
    private IRPRuleProcessor ruleProcessor;
    /** The place where the objects are stored */
    private RPWorld world;
    private Statistics stats;
    private SimulationStats simulationStats;
    /** The networkServerManager so that we can send perceptions */
    private INetworkServerManager netMan;
    /** The PlayerEntryContainer so that we know where to send perceptions */
    private PlayerEntryContainer playerContainer;
    private List<PlayerEntry> playersToRemove;
    private Map<RPObject, List<TransferContent>> contentsToTransfer;

    /**
     * Constructor
     *
     * @param netMan
     *            the NetworkServerManager so that we can send message
     * @throws Exception in case of an unexpected error
     */
    public RPServerManager(INetworkServerManager netMan) throws Exception {
        super("RPServerManager");

        try {
            stats = Statistics.getStatistics();
            simulationStats = SimulationStats.getStatistics();
            keepRunning = true;
            isfinished = false;

            scheduler = new RPScheduler();
            contentsToTransfer = new HashMap<RPObject, List<TransferContent>>();
            playerContainer = PlayerEntryContainer.getContainer();

            playersToRemove = new LinkedList<PlayerEntry>();
            this.netMan = netMan;

            Configuration conf = Configuration.getConfiguration();
            /*
             * This method loads the extensions that implement the game server
             * code.
             */
            initializeExtensions(conf);

            String duration = conf.get("turn_length");
            turnDuration = Long.parseLong(duration);
            turn = 0;
        } catch (Exception e) {
            logger.warn("ABORT: Unable to create RPZone, RPRuleProcessor or RPAIManager instances",
                    e);
            throw e;
        }
    }

    /**
     * This method loads the extensions: IRPRuleProcessor and IRPWorld that are
     * going to be used to implement your game. This method loads these class
     * from the class names passed as arguments in Configuration
     *
     * @param conf
     *            the Configuration class
     * @throws ClassNotFoundException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws SecurityException
     * @throws IllegalArgumentException
     */
    protected void initializeExtensions(Configuration conf) throws ClassNotFoundException,
            IllegalArgumentException, SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        Class<?> worldClass = Class.forName(conf.get("world"));
        // call the get() method without parameters to retrieve the singleton
        // instance
        world = (RPWorld) worldClass.getDeclaredMethod("get", new Class[0]).invoke(null, (Object[]) null);
        RPWorld.set(world);
        world.onInit();

        Class<?> ruleProcessorClass = Class.forName(conf.get("ruleprocessor"));
        // call the get() method without parameters to retrieve the singleton
        // instance
        ruleProcessor = (IRPRuleProcessor) ruleProcessorClass.getDeclaredMethod("get", new Class[0]).invoke(null, (Object[]) null);
        ruleProcessor.setContext(this);
    }

    /**
     * This method returns the actual turn number.
     *
     * @return actual turn number
     */
    public int getTurn() {
        return turn;
    }

    /**
     * gets the duration of a turn
     *
     * @return duration
     */
    public long getTurnDuration() {
        return turnDuration;
    }

    /**
     * This method finishes the thread that runs the RPServerManager. It calls the
     * RPWorld.onFinish() method.
     */
    public void finish() {
        keepRunning = false;
        simulationStats.getOutputFile().close();
        while (isfinished == false) {
            Thread.yield();
        }

        try {
            world.onFinish();
        } catch (Exception e) {
            logger.error("error while finishing RPServerManager", e);
        }
    }

    /**
     * Adds an action for the next turn
     *
     * @param object
     *            the object that casted the action
     * @param action
     *            the action itself
     * @throws ActionInvalidException
     */
    public void addRPAction(RPObject object, RPAction action) throws ActionInvalidException {
        if (logger.isDebugEnabled()) {
            logger.debug("Added action: " + action);
        }

        scheduler.addRPAction(object, action, ruleProcessor);
    }

    /**
     * This method decide if an client runs a compatible version of the game
     *
     * @param game
     *            the game name
     * @param version
     *            the game version as a string
     * @return true if it is compatible.
     */
    public boolean checkGameVersion(String game, String version) {
        return ruleProcessor.checkGameVersion(game, version);
    }

    /**
     * Creates an account for a player in the game.
     *
     * @param username
     *            player's username
     * @param password
     *            player's password
     * @param email
     *            player's email
     * @param address
     *            ip address of client
     * @return a Result indicating if account creation was done successfully or not.
     */
    public AccountResult createAccount(String username, String password, String email, String address) {
        try {
            if (!Boolean.parseBoolean(Configuration.getConfiguration().get("allow_account_creation", "true"))) {
                return new AccountResult(Result.FAILED_CREATE_ON_MAIN_INSTEAD, username);
            }
        } catch (IOException e) {
            logger.error(e, e);
        }

        // check account creation limits
        try {
            if (DAORegister.get().get(AccountDAO.class).isAccountCreationLimitReached(address)) {
                return new AccountResult(Result.FAILED_TOO_MANY, username);
            }
        } catch (SQLException e) {
            logger.error(e, e);
            return new AccountResult(Result.FAILED_EXCEPTION, username);
        } catch (IOException e) {
            logger.error(e, e);
            return new AccountResult(Result.FAILED_EXCEPTION, username);
        }

        // forward the creation request to the game
        return ruleProcessor.createAccount(username, password, email);
    }

    /**
     * Creates a character for a account of a player
     *
     * @param username
     *            player's username
     * @param character
     * @param template
     *            the template we are going to use to create the object.
     * @param address
     *            ip address of client
     * @return a Result indicating if account creation was done successfully or
     *         if it is not the cause.
     */
    public CharacterResult createCharacter(String username, String character, RPObject template, String address) {
        try {
            if (!Boolean.parseBoolean(Configuration.getConfiguration().get("allow_account_creation", "true"))) {
                return new CharacterResult(Result.FAILED_CREATE_ON_MAIN_INSTEAD, character, template);
            }
        } catch (IOException e) {
            logger.error(e, e);
        }

        // check account creation limits
        try {
            if (DAORegister.get().get(CharacterDAO.class).isCharacterCreationLimitReached(username, address)) {
                return new CharacterResult(Result.FAILED_TOO_MANY, character, template);
            }
        } catch (SQLException e) {
            logger.error(e, e);
            return new CharacterResult(Result.FAILED_EXCEPTION, character, template);
        } catch (IOException e) {
            logger.error(e, e);
            return new CharacterResult(Result.FAILED_EXCEPTION, character, template);
        }
        return ruleProcessor.createCharacter(username, character, template);
    }

    private Perception getPlayerPerception(PlayerEntry entry) {
        Perception perception = null;

        IRPZone.ID id = new IRPZone.ID(entry.object.get("zoneid"));
        IRPZone zone = world.getRPZone(id);

        if (entry.requestedSync == false) {
            perception = zone.getPerception(entry.object, Perception.DELTA);
        } else {
            entry.requestedSync = false;
            perception = zone.getPerception(entry.object, Perception.SYNC);
        }

        return perception;
    }

    private void sendPlayerPerception(PlayerEntry entry, Perception perception, RPObject playerObject) {
        if (perception == null) {
            /** Until player enters game perception is null */
            return;
        }

        MessageS2CPerception messages2cPerception = new MessageS2CPerception(entry.channel,
                perception);

        stats.add("Perceptions " + (perception.type == 0 ? "DELTA" : "SYNC"), 1);
        simulationStats.add("Perception Type", (perception.type == 0 ? "DELTA" : "SYNC"));
        /*
         * The perception is build of two parts: the general information and the
         * private information about our object. This private information
         * consists only of attributes that are not visible to every player but
         * the owner, because visible attributes are already stored in the
         * perception.
         */

        if (perception.type == Perception.SYNC) {
            RPObject copy = new RPObject();
            copy.fill(playerObject);
            if (!playerObject.isHidden()) {
                copy.clearVisible(true);
            }
            messages2cPerception.setMyRPObject(copy, null);
        } else {
            RPObject added = new RPObject();
            RPObject deleted = new RPObject();

            try {
                playerObject.getDifferences(added, deleted);
                if (!playerObject.isHidden()) {
                    added.clearVisible(false);
                    deleted.clearVisible(false);
                }

                if (added.size() == 0) {
                    added = null;
                }

                if (deleted.size() == 0) {
                    deleted = null;
                }
            } catch (Exception e) {
                logger.error("Error getting object differences", e);
                logger.error(playerObject);
                added = null;
                deleted = null;
            }
            messages2cPerception.setMyRPObject(added, deleted);
        }
        messages2cPerception.setWorldSize(MaPacmanRPWorld.get().getPlayers().size());
        messages2cPerception.setClientID(entry.clientid);
        if (simulationStats.get("client_id") != null && Integer.parseInt(simulationStats.get("client_id")) == entry.clientid) {
            simulationStats.add("player_name", entry.username);
            simulationStats.add("server_sent", "" + System.currentTimeMillis());
            simulationStats.add("client_id", "" + entry.clientid);
            simulationStats.add("message_type", messages2cPerception.getType().name());
            simulationStats.add("response_time", "" + (Long.parseLong(simulationStats.get("server_sent")) - Long.parseLong(simulationStats.get("server_recieved"))));
            simulationStats.add("ID", "1");
            //if(simulationStats.get("Bytes recv").compareTo("0")!=0)
            simulationStats.print();
        }
        messages2cPerception.setPerceptionTimestamp(entry.getPerceptionTimestamp());
        messages2cPerception.setProtocolVersion(entry.getProtocolVersion());

        netMan.sendMessage(messages2cPerception);
    }

    private void buildPerceptions() {
        playersToRemove.clear();

        /** We reset the cache at Perceptions */
        MessageS2CPerception.clearPrecomputedPerception();
        PlayerEntry tempEntry = null;
        long mStart = 0;
        for (PlayerEntry entry : playerContainer) {
            try {
                if (entry.object != null) {
                    if (entry.object.get("name").compareTo("char1") == 0 &&  Integer.parseInt(SimulationStats.getStatistics().get("migration_size")) >  0) {
                        tempEntry = entry;
                        tempEntry.state = ClientState.LOGOUT_ACCEPTED;
                        ZoneDivider.get().setMigrate(false);
                    }
                }
                // Before creating the perception we check the player is still there.
                if (entry.isTimeout()) {
                    logger.info("Request (TIMEOUT) disconnection of Player " + entry.channel);
                    playersToRemove.add(entry);
                    continue;
                }

                if (entry.state == ClientState.GAME_BEGIN) {
                    //playerContainer.getLock().requestReadLock();

                    Perception perception = getPlayerPerception(entry);
                    sendPlayerPerception(entry, perception, entry.object);
                    //playerContainer.getLock().releaseLock();
                    continue;
                } else if (entry.state == ClientState.MIGRATE) {
                    if (mStart == 0){
                        mStart = System.currentTimeMillis();
                        simulationStats.add("m_start", ""+mStart);
                    }
                    MessageS2CMigrate msg = new MessageS2CMigrate(entry.channel);
                    //msg.setEntry(entry);
                    msg.setClientID(entry.clientid);
                    //playerContainer.getLock().requestReadLock();
                    msg.setClientName(entry.object.get("name"));
                    //playerContainer.getLock().releaseLock();
                    msg.setIpToConnect(ZoneDivider.get().getIPBasedOnPosition(entry.object));
                    netMan.sendMessage(msg);
                    if (simulationStats.get("client_id") != null && Integer.parseInt(simulationStats.get("client_id")) == entry.clientid) {
                        simulationStats.add("player_name", entry.username);
                        simulationStats.add("server_sent", "" + System.currentTimeMillis());
                        simulationStats.add("client_id", "" + entry.clientid);
                        simulationStats.add("message_type", msg.getType().name());
                        simulationStats.add("response_time", "0");
                        simulationStats.add("ID", "1");
                        //if(simulationStats.get("Bytes recv").compareTo("0")!=0)
                        simulationStats.print();
                    }
                }
            } catch (Exception e) {
                logger.error("Removing player(" + entry.clientid + ") because it caused a Exception while contacting it", e);
                playersToRemove.add(entry);
            }

        }

        if (tempEntry != null) {
            MessageS2CMigrate msg = new MessageS2CMigrate(tempEntry.channel);
            //msg.setEntry(entry);
            msg.setClientID(tempEntry.clientid);
            //playerContainer.getLock().requestReadLock();
            msg.setClientName(tempEntry.object.get("name"));
            //playerContainer.getLock().releaseLock();
            msg.setIpToConnect("");

            netMan.sendMessage(msg);
        }


        for (PlayerEntry entry : playersToRemove) {
            logger.warn("RP Disconnecting entry: " + entry);
            netMan.disconnectClient(entry.channel);
        }
    }

    /** This method is called when a player is added to the game */
    public boolean onInit(RPObject object) throws RPObjectInvalidException {
        return ruleProcessor.onInit(object);
    }

    /** This method is called when a player leaves the game */
    public boolean onExit(RPObject object) throws RPObjectNotFoundException {
        scheduler.clearRPActions(object);
        contentsToTransfer.remove(object);

        return ruleProcessor.onExit(object);
    }

    /** This method is called when connection to client is closed */
    public void onTimeout(RPObject object) throws RPObjectNotFoundException {
        scheduler.clearRPActions(object);
        contentsToTransfer.remove(object);

        ruleProcessor.onTimeout(object);
    }

    private void deliverTransferContent() {
        synchronized (contentsToTransfer) {
            for (Map.Entry<RPObject, List<TransferContent>> val : contentsToTransfer.entrySet()) {
                RPObject target = val.getKey();
                List<TransferContent> content = val.getValue();

                PlayerEntry entry = playerContainer.get(target);
                if (entry == null) {
                    logger.warn("Entry for player (" + target + ") does not exist: " + playerContainer, new Throwable());
                    continue;
                }


                if (content == null) {
                    logger.warn("content is null");
                }
                if (!entry.contentToTransfer.isEmpty()) {
                    // prevent DoS if the client never confirms the Transfer offer
                    if (entry.contentToTransfer.size() > 30) {
                        synchronized (entry.contentToTransfer) {
                            for (int i = 0; i < 10; i++) {
                                entry.contentToTransfer.remove(0);
                            }
                        }
                    }
                    logger.warn("Adding to existing contentToTransfer for player " + entry.character + " old: " + entry.contentToTransfer + " added " + content);
                }
                entry.contentToTransfer.addAll(content);

                MessageS2CTransferREQ mes = new MessageS2CTransferREQ(entry.channel, content);
                mes.setClientID(entry.clientid);
                simulationStats.add("player_name", entry.username);

                mes.setProtocolVersion(entry.getProtocolVersion());

                netMan.sendMessage(mes);
            }

            contentsToTransfer.clear();
        }
    }

    /** This method is triggered to send content to the clients */
    public void transferContent(RPObject target, List<TransferContent> content) {
        synchronized (contentsToTransfer) {
            contentsToTransfer.put(target, content);
        }
    }

    /** This method is triggered to send content to the clients */
    public void transferContent(RPObject target, TransferContent content) {
        List<TransferContent> list = new LinkedList<TransferContent>();
        list.add(content);

        transferContent(target, list);
    }

    @Override
    public void run() {
        try {
            long start = System.nanoTime();
            long stop;
            long delay;
            long timeStart = 0;
            long[] timeEnds = new long[12];

            while (keepRunning) {
                stop = System.nanoTime();

                logger.debug("Turn time elapsed: " + ((stop - start) / 1000) + " microsecs");
                delay = turnDuration - ((stop - start) / 1000000);
                if (delay < 0) {
                    StringBuilder sb = new StringBuilder();
                    for (long timeEnd : timeEnds) {
                        sb.append(" " + (timeEnd - timeStart));
                    }

                    logger.warn("Turn duration overflow by " + (-delay) + " ms: "
                            + sb.toString());
                } else if (delay > turnDuration) {
                    logger.error("Delay bigger than Turn duration. [delay: " + delay
                            + "] [turnDuration:" + turnDuration + "]");
                    delay = 0;
                }

                // only sleep when the turn delay is > 0
                if (delay > 0) {
                    try {
                        Thread.sleep(delay);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                }

                start = System.nanoTime();
                timeStart = System.currentTimeMillis();

                playerContainer.getLock().requestWriteLock();

                try {
                    timeEnds[0] = System.currentTimeMillis();

                    /* Get actions that players send */
                    scheduler.nextTurn();
                    timeEnds[1] = System.currentTimeMillis();

                    /* Execute them all */
                    scheduler.visit(ruleProcessor);
                    timeEnds[2] = System.currentTimeMillis();

                    /* Compute game RP rules to move to the next turn */
                    ruleProcessor.endTurn();
                    timeEnds[3] = System.currentTimeMillis();

                    /* Send content that is waiting to players */
                    deliverTransferContent();
                    timeEnds[4] = System.currentTimeMillis();

                    /* Tell player what happened */
                    buildPerceptions();
                    timeEnds[5] = System.currentTimeMillis();

                    /* save players regularly to the db */
                    savePlayersPeriodicly();
                    timeEnds[6] = System.currentTimeMillis();

                    /* Move zone to the next turn */
                    world.nextTurn();
                    timeEnds[7] = System.currentTimeMillis();

                    turn++;
//                    System.err.println("requesting turn for everyone....");
                    ZoneDivider.get().resetPopulation();
                    ruleProcessor.beginTurn();
                    ZoneDivider.get().divideZoneStrategy();
//                    System.err.println("finishing turn for everyone....");

                    timeEnds[8] = System.currentTimeMillis();
                } finally {
                    playerContainer.getLock().releaseLock();
                    timeEnds[9] = System.currentTimeMillis();
                }
                try {
                    stats.set("Objects now", world.size());
                    simulationStats.set("nObjects", "" + world.size());
                } catch (ConcurrentModificationException e) {
                    //TODO: size is obviously not threadsafe as it asks the underlying zone.objects for its sizes, which are not threadsafe.
                    e.printStackTrace();
                }
                timeEnds[10] = System.currentTimeMillis();
                TransactionPool.get().kickHangingTransactionsOfThisThread();
                timeEnds[11] = System.currentTimeMillis();
            }
        } catch (Throwable e) {
            logger.error("Unhandled exception, server will shut down.", e);
        } finally {
            isfinished = true;
        }
    }

    private void savePlayersPeriodicly() {
        for (PlayerEntry entry : playerContainer) {
            try {
                // do not use = 0 because we need a little time until the
                // player object is fully initialized (e. g. has a charname)
                if (entry.getThisPerceptionTimestamp() % 2000 == 1999) {
                    entry.storeRPObject(entry.object);
                }
            } catch (Exception e) {
                String name = "null";
                if (entry != null) {
                    name = entry.character;
                }
                logger.error("Error while storing player " + name, e);
            }
        }
    }

    /**
     * This method disconnects a player from the server.
     *
     * @param object
     *            the player object that we want to disconnect from world
     */
    public void disconnectPlayer(RPObject object) {
        PlayerEntry entry = playerContainer.get(object);
        if (entry == null) {
            /*
             * There is no player entry for such channel This is not necesaryly
             * an error, as the connection could be anything else but an arianne
             * client or we are just disconnecting a player that logout
             * correctly.
             */
            logger.warn("There is no PlayerEntry associated to this RPObject.");
            return;
        }

        netMan.disconnectClient(entry.channel);
    }

    /**
     * This method exposes network layer connection validator so game logic can
     * handle it.
     *
     * @return the connection validator
     */
    public ConnectionValidator getValidator() {
        return netMan.getValidator();
    }
}
